Skip to content

OZMO 905 Support#883

Closed
nanomad wants to merge 28 commits into
DeebotUniverse:devfrom
nanomad:feature/2pv572-support
Closed

OZMO 905 Support#883
nanomad wants to merge 28 commits into
DeebotUniverse:devfrom
nanomad:feature/2pv572-support

Conversation

@nanomad

@nanomad nanomad commented Apr 6, 2025

Copy link
Copy Markdown
Contributor

This PR adds support for the OZMO 900 bot, a legacy XML robot.

The work is rebased on top of #817 and #560

@nanomad nanomad marked this pull request as draft April 6, 2025 10:12
@nanomad nanomad force-pushed the feature/2pv572-support branch from bcfeaf6 to ea5b144 Compare April 6, 2025 10:52
@nanomad nanomad marked this pull request as ready for review April 6, 2025 11:11
@nanomad nanomad changed the title [WIP] OZMO 900 Support OZMO 900 Support Apr 6, 2025
@nanomad nanomad changed the title OZMO 900 Support OZMO 905 Support Apr 6, 2025
@flubshi

flubshi commented Apr 6, 2025

Copy link
Copy Markdown
Contributor

Nice 😻👍

Comment thread deebot_client/commands/xml/water_info.py Outdated
Comment thread deebot_client/hardware/deebot/2pv572.py Outdated
@flubshi

flubshi commented Apr 7, 2025

Copy link
Copy Markdown
Contributor

Did you runtime test the SetCleanSpeed command? Not sure if my setup is broken, but for my XML robot an error is thrown:

await bot.execute_command(SetCleanSpeed(FanSpeedLevel.MAX))

DEBUG:deebot_client.authentication:Calling api(1/3): url=https://portal-eu.ecouser.net/api/iot/devmanager.do, params={'mid': 'ls1ok3', 'did': '56[...]-[...]a8a', 'td': 'q', 'u': 'ia[...]10', 'cv': '1.67.3', 't': 'a', 'av': '1.3.1'}, json={'cmdName': 'SetCleanSpeed', 'payload': '<ctl speed="strong" />', 'payloadType': 'x', 'td': 'q', 'toId': '56[...]-[...]a8a', 'toRes': 'Y1OW', 'toType': 'ls1ok3'}
DEBUG:deebot_client.mqtt_client.client:Received PUBLISH (d0, q0, r0, m0), 'iot/p2p/SetCleanSpeed/HelperMQClientId-sts-ngiot-mqserver-eco0-54/ecosys/1234/56[...]-[...]a8a/ls1ok3/Y1OW/q/K3bN/x', ...  (22 bytes)
DEBUG:deebot_client.mqtt_client:Got message: topic=iot/p2p/SetCleanSpeed/HelperMQClientId-sts-ngiot-mqserver-eco0-54/ecosys/1234/56[...]-[...]a8a/ls1ok3/Y1OW/q/K3bN/x, payload=b'<ctl speed="strong" />'
DEBUG:deebot_client.mqtt_client.client:Received PUBLISH (d0, q0, r0, m0), 'iot/p2p/SetCleanSpeed/56[...]-[...]a8a/ls1ok3/Y1OW/HelperMQClientId-sts-ngiot-mqserver-eco0-54/ecosys/1234/p/K3bN/x', ...  (15 bytes)
DEBUG:deebot_client.mqtt_client:Got message: topic=iot/p2p/SetCleanSpeed/56[...]-[...]a8a/ls1ok3/Y1OW/HelperMQClientId-sts-ngiot-mqserver-eco0-54/ecosys/1234/p/K3bN/x, payload=b"<ctl ret='ok'/>"
WARNING:deebot_client.message:Could not parse SetCleanSpeed: b"<ctl ret='ok'/>"
Traceback (most recent call last):
  File "/usr/lib/python3.13/xml/etree/ElementTree.py", line 1713, in feed
    self.parser.Parse(data, False)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
xml.parsers.expat.ExpatError: not well-formed (invalid token): line 1, column 1

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "client.py/deebot_client/message.py", line 65, in wrapper
    response = func(cls, event_bus, data)
  File "client.py/deebot_client/message.py", line 108, in handle
    return cls._handle(event_bus, message)
           ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "client.py/deebot_client/message.py", line 138, in _handle
    return cls.__handle_str(event_bus, message)
           ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "client.py/deebot_client/message.py", line 126, in __handle_str
    return cls._handle_str(event_bus, message)
           ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "client.py/deebot_client/commands/xml/common.py", line 67, in _handle_str
    xml = ElementTree.fromstring(message)
  File "client.py/.venv/lib/python3.13/site-packages/defusedxml/common.py", line 126, in fromstring
    parser.feed(text)
    ~~~~~~~~~~~^^^^^^
  File "/usr/lib/python3.13/xml/etree/ElementTree.py", line 1715, in feed
    self._raiseerror(v)
    ~~~~~~~~~~~~~~~~^^^
  File "/usr/lib/python3.13/xml/etree/ElementTree.py", line 1622, in _raiseerror
    raise err
xml.etree.ElementTree.ParseError: not well-formed (invalid token): line 1, column 1

⬆️ fixed by 17d49a0

Edit: And mypy seems unhappy too:

uv run --frozen mypy deebot_client/

⬆️ fixed

This comment is resolved

@nanomad

nanomad commented Apr 7, 2025

Copy link
Copy Markdown
Contributor Author

Did you runtime test the SetCleanSpeed command? Not sure if my setup is broken, but for my XML robot an error is thrown:

await bot.execute_command(SetCleanSpeed(FanSpeedLevel.MAX))

DEBUG:deebot_client.authentication:Calling api(1/3): url=https://portal-eu.ecouser.net/api/iot/devmanager.do, params={'mid': 'ls1ok3', 'did': '56[...]-[...]a8a', 'td': 'q', 'u': 'ia[...]10', 'cv': '1.67.3', 't': 'a', 'av': '1.3.1'}, json={'cmdName': 'SetCleanSpeed', 'payload': '<ctl speed="strong" />', 'payloadType': 'x', 'td': 'q', 'toId': '56[...]-[...]a8a', 'toRes': 'Y1OW', 'toType': 'ls1ok3'}
DEBUG:deebot_client.mqtt_client.client:Received PUBLISH (d0, q0, r0, m0), 'iot/p2p/SetCleanSpeed/HelperMQClientId-sts-ngiot-mqserver-eco0-54/ecosys/1234/56[...]-[...]a8a/ls1ok3/Y1OW/q/K3bN/x', ...  (22 bytes)
DEBUG:deebot_client.mqtt_client:Got message: topic=iot/p2p/SetCleanSpeed/HelperMQClientId-sts-ngiot-mqserver-eco0-54/ecosys/1234/56[...]-[...]a8a/ls1ok3/Y1OW/q/K3bN/x, payload=b'<ctl speed="strong" />'
DEBUG:deebot_client.mqtt_client.client:Received PUBLISH (d0, q0, r0, m0), 'iot/p2p/SetCleanSpeed/56[...]-[...]a8a/ls1ok3/Y1OW/HelperMQClientId-sts-ngiot-mqserver-eco0-54/ecosys/1234/p/K3bN/x', ...  (15 bytes)
DEBUG:deebot_client.mqtt_client:Got message: topic=iot/p2p/SetCleanSpeed/56[...]-[...]a8a/ls1ok3/Y1OW/HelperMQClientId-sts-ngiot-mqserver-eco0-54/ecosys/1234/p/K3bN/x, payload=b"<ctl ret='ok'/>"
WARNING:deebot_client.message:Could not parse SetCleanSpeed: b"<ctl ret='ok'/>"
Traceback (most recent call last):
  File "/usr/lib/python3.13/xml/etree/ElementTree.py", line 1713, in feed
    self.parser.Parse(data, False)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
xml.parsers.expat.ExpatError: not well-formed (invalid token): line 1, column 1

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "client.py/deebot_client/message.py", line 65, in wrapper
    response = func(cls, event_bus, data)
  File "client.py/deebot_client/message.py", line 108, in handle
    return cls._handle(event_bus, message)
           ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "client.py/deebot_client/message.py", line 138, in _handle
    return cls.__handle_str(event_bus, message)
           ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "client.py/deebot_client/message.py", line 126, in __handle_str
    return cls._handle_str(event_bus, message)
           ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "client.py/deebot_client/commands/xml/common.py", line 67, in _handle_str
    xml = ElementTree.fromstring(message)
  File "client.py/.venv/lib/python3.13/site-packages/defusedxml/common.py", line 126, in fromstring
    parser.feed(text)
    ~~~~~~~~~~~^^^^^^
  File "/usr/lib/python3.13/xml/etree/ElementTree.py", line 1715, in feed
    self._raiseerror(v)
    ~~~~~~~~~~~~~~~~^^^
  File "/usr/lib/python3.13/xml/etree/ElementTree.py", line 1622, in _raiseerror
    raise err
xml.etree.ElementTree.ParseError: not well-formed (invalid token): line 1, column 1

I did but turns out sometimes it replies with bytes or bytearray (paho can do that). I've added the decoding to the XML message as well as I did for events altough I'm not sure wheter that decoding should be pushed further up the chain

@flubshi

flubshi commented Apr 7, 2025

Copy link
Copy Markdown
Contributor

I'm not sure wheter that decoding should be pushed further up the chain

🤷‍♂️
Maybe @edenhaus as the maintainer can answer this.

@nanomad nanomad force-pushed the feature/2pv572-support branch from 4fd891e to 00948d6 Compare April 7, 2025 20:34
@nanomad

nanomad commented Apr 7, 2025

Copy link
Copy Markdown
Contributor Author

TODO List:

  • Clean job end state decoding (success / stop / error)
  • Figure out how HA fetches the "clean duration" statistics
  • Network Diagnostics

Comment thread deebot_client/hardware/deebot/2pv572.py Outdated
),
network=CapabilityEvent(NetworkInfoEvent, []),
play_sound=CapabilityExecute(PlaySound),
settings=CapabilitySettings(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the settings key is required:

  File "client.py/deebot_client/hardware/deebot/2pv572.py", line 55, in <module>
    Capabilities(
    ~~~~~~~~~~~~^
        availability=CapabilityEvent(AvailabilityEvent, []),
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<33 lines>...
        ),
        ^^
    ),
    ^
TypeError: Capabilities.__init__() missing 1 required keyword-only argument: 'settings'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@flubshi That's correct but we do not support any of those settings yet. In particular the volume seems mandatory.
I'm going to push a "fake volume" command tomorrow so that both HA and the library are happy :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Actually, my bot doesn't support ANY volume configuration at all)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML robot Deebot 900 has also no volume configuration. Maybe we should make it optional in the library instead of a fake module.

Comment thread deebot_client/hardware/deebot/2pv572.py
@nanomad

nanomad commented Apr 13, 2025

Copy link
Copy Markdown
Contributor Author

@flubshi I've added map support as well, feel free to check if it works for you as well

@flubshi

flubshi commented Apr 13, 2025

Copy link
Copy Markdown
Contributor

@flubshi I've added map support as well, feel free to check if it works for you as well

Wow, yes - it works! Thanks a lot!
I modified my ls1ok3.py based on your 2pv572.py (it is the same, except for the waterinfo).

The map of the bot is now available in Home Assistant :)

image

Most sensors are working as well.
Not sure how/whether the room info is displayed in Home Assistant, but using the room IDs from API to clean a specific room works as expected. I was able to clean the kitchen via voice assistant.

@nanomad nanomad force-pushed the feature/2pv572-support branch from f6abb2c to 336346f Compare April 14, 2025 19:36
@nanomad

nanomad commented Apr 14, 2025

Copy link
Copy Markdown
Contributor Author

@flubshi two new features for you to test:

  • HA should now display the bot trace in overlay
  • HA should rebuild the map in realtime while the bot is cleaning (as the app does)

@edenhaus edenhaus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution :)
This PR is huge and needs to be splitted into multiple PRs.
Please create a PR for any new added command. Similar commands like all map commands can be in one PR.
The last PR should be the PR, where the capabilities files for the new bot is added.
As the capabilities file changes include breaking change, I want that PR as small as possible.
A lot of test are missing for the newly added commands. Please add them

Comment thread deebot_client/command.py Outdated
except ValueError as err:
msg = f'Could not convert "{value}" of {name} into {type_}'
raise DeebotError(msg) from err
if hasattr(type_, "from_xml"):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is xml specific and should be in the XML specific files and not in the general one

Comment thread deebot_client/commands/json/network.py Outdated
status = State.DOCKED
case "idle":
status = State.IDLE
pass

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you changing this line?

elif clean_action == CleanAction.PAUSE:
event_bus.notify(StateEvent(State.PAUSED))
else:
_LOGGER.debug("Ignored CleanState %s", clean_action)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we ignoring this state? If we don't know what this state means, then we should return HandlingResult.analyse() instead



class XmlSetCommand(ExecuteCommand, SetCommand, ABC):
class XmlCommandMqttP2P(XmlCommand, CommandMqttP2P, ABC):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommandMqttP2P should only be used for commands, where the bot will not inform us if the command was changed via the app. Newer models are always sending a message if the state has changed and therefore this class is somehow deprecated and only used if the bot really doesn't send any updates

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed I agree, I'll get rid of this as I see no value. We do get notification via messages for most of the changes anyway. The only thing I'm not so sure about is SetOnOff (which I have locally and has no message counterpart, but I'll leave that for another PR)

Comment thread deebot_client/events/map.py

amount: WaterAmount
# None means no data available
amount: WaterAmount | None = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is the amount None and another field not?

@nanomad nanomad Apr 19, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XML bots have two commands:
One to tell you if the waterbox is attached (GetWaterBoxInfo), the other to give you the water level (GetWaterPermeability) . So we cannot always publish the water amount

Comment thread deebot_client/map.py
Comment thread deebot_client/util/__init__.py
@nanomad nanomad marked this pull request as draft April 19, 2025 08:16
@nanomad nanomad force-pushed the feature/2pv572-support branch 2 times, most recently from d022c86 to a6fc2d2 Compare April 19, 2025 11:11
@nanomad nanomad force-pushed the feature/2pv572-support branch from a6fc2d2 to b22f593 Compare April 22, 2025 13:38
@nanomad

nanomad commented Apr 29, 2025 via email

Copy link
Copy Markdown
Contributor Author

@nanomad nanomad closed this Apr 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants